﻿using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Text;

namespace Faml.Frontend
{
    public enum BinaryOperations : byte
    {
        Add, 
        Subtract, 
        Multiply,
        Divide,
        Modulo,
        And,
        Or,
        GreaterThan,
        LessThan,
        GreaterThanOrEqual,
        LessThanOrEqual,
        Equal,
        NotEqual,
    }

    public class FamlBinaryExpression : FamlExpression
    {
        private FamlExpression left;
        private FamlExpression right;
        private BinaryOperations op;

        public FamlBinaryExpression(FamlExpression left, FamlExpression right, BinaryOperations op)
        {
            this.left = left;
            this.right = right;
            this.op = op;
            MakeMetadata();
        }

        public FamlBinaryExpression(FamlExpression left, FamlExpression right, string op):
            this(left, right, StringToOperation(op))
        {
        }

        public FamlExpression Left
        {
            get
            {
                return left;
            }
        }

        public FamlExpression Right
        {
            get
            {
                return right;
            }
        }

        public BinaryOperations Operator
        {
            get
            {
                return op;
            }
        }

        private string OperatorAsString
        {
            get { return OperationToString(op); }
        }

        private static BinaryOperations StringToOperation(string op)
        {
            switch (op)
            {
                case "+": return BinaryOperations.Add;
                case "-": return BinaryOperations.Subtract;
                case "*": return BinaryOperations.Multiply;
                case "/": return BinaryOperations.Divide;
                case "%": return BinaryOperations.Modulo;
                case "&&": return BinaryOperations.And;
                case "||": return BinaryOperations.Or;
                case ">": return BinaryOperations.GreaterThan;
                case "<": return BinaryOperations.LessThan;
                case ">=": return BinaryOperations.GreaterThanOrEqual;
                case "<=": return BinaryOperations.LessThanOrEqual;
                case "==": return BinaryOperations.Equal;
                case "!=": return BinaryOperations.NotEqual;
                default: throw new ApplicationException("Unsupported binary operation: " + op);
            }
        }

        public static string OperationToString(BinaryOperations op)
        {
            switch (op)
            {
                case BinaryOperations.Add:
                    return "+";
                case BinaryOperations.Subtract:
                    return "-";
                case BinaryOperations.Multiply:
                    return "*";
                case BinaryOperations.Divide:
                    return "/";
                case BinaryOperations.Modulo:
                    return "%";
                case BinaryOperations.And:
                    return "&&";
                case BinaryOperations.Or:
                    return "||";
                case BinaryOperations.GreaterThan:
                    return ">";
                case BinaryOperations.LessThan:
                    return "<";
                case BinaryOperations.GreaterThanOrEqual:
                    return ">=";
                case BinaryOperations.LessThanOrEqual:
                    return "<=";
                case BinaryOperations.Equal:
                    return "==";
                case BinaryOperations.NotEqual:
                    return "!=";
                default:
                    return "??";
            }
        }

        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();
            if (left is FamlBinaryExpression)
            {
                BinaryOperations leftOp = ((FamlBinaryExpression)left).Operator;
                if (OperatorPrecedence.Get(op) > OperatorPrecedence.Get(leftOp))
                    sb.AppendFormat("({0})", left.ToString());
                else
                    sb.Append(left.ToString());
            }
            else
                sb.Append(left.ToString());
            sb.Append(OperatorAsString);
            if (right is FamlBinaryExpression)
            {
                BinaryOperations rightOp = ((FamlBinaryExpression)right).Operator;
                if (OperatorPrecedence.Get(op) > OperatorPrecedence.Get(rightOp))
                    sb.AppendFormat("({0})", right.ToString());
                else
                    sb.Append(right.ToString());
            }
            else
                sb.Append(right.ToString());
            return sb.ToString();
        }

        public override FamlExpression Expand()
        {
            // TODO: perform type compatibility check here
            return new FamlBinaryExpression(left.Expand(), right.Expand(), op);
        }

        protected override Type ComputeType()
        {
            switch (this.op)
            {
                case BinaryOperations.Add:
                case BinaryOperations.Subtract:
                case BinaryOperations.Multiply:
                case BinaryOperations.Divide:
                case BinaryOperations.Modulo:
                    if (left.Type == typeof(double) && right.Type == typeof(int))
                        return typeof(double); // Perform type promotion
                    else if (right.Type == typeof(double) && left.Type == typeof(int))
                        return typeof(double); // Perform type promotion
                    else if (left.Type == typeof(DateTime))
                    {
                        if (this.op == BinaryOperations.Subtract)
                        {
                            if (right.Type != left.Type)
                                throw new Exception("Expected for DateTime");
                            return typeof(TimeSpan);                            
                        }
                    }
                    return left.Type; // TODO: check type compatibility

                case BinaryOperations.And:
                case BinaryOperations.Or:
                    if (left.Type != typeof(Boolean) || right.Type != typeof(Boolean))
                        throw new ApplicationException("Expected for boolean type");
                    return typeof(Boolean);

                case BinaryOperations.GreaterThan:
                case BinaryOperations.LessThan:
                case BinaryOperations.GreaterThanOrEqual:
                case BinaryOperations.LessThanOrEqual:
                case BinaryOperations.Equal:
                case BinaryOperations.NotEqual:
                    return typeof(Boolean);
                
                
                default:
                    throw new ApplicationException("Unexpected binary operation " + this.op);
            }
        }

        protected override void MakeMetadata()
        {
            int height = 1;
            int hash = NodeType.GetHashCode() ^ (op.GetHashCode() << 4);

            hash ^= left.Metadata.Hash;
            height = left.Metadata.Height + 1;
            this.metadata.AddIfNotExist(left.NodeParameters);

            hash ^= right.Metadata.Hash;
            height = Math.Max(height, right.Metadata.Height + 1);
            this.metadata.AddIfNotExist(right.NodeParameters);

            this.metadata.Height = height;
            this.metadata.Hash = hash;
        }

        public override FamlExpressionType NodeType
        {
            get { return FamlExpressionType.BinaryExpression; }
        }

        public override IEnumerable<FamlExpression> Children
        {
            get 
            {
                yield return left;
                yield return right;
            }
        }
    }
}